# coding: utf-8

import copy
import datetime

from .exceptions import (
    ModelAttrNotDefineError
)
from .query import Query


def validate_model_and_fields(db, table, fields: list) -> bool:
    db.cursor.execute("DESC %s" % table)
    ret = db.cursor.fetchall()
    db_fields = {r[0] for r in ret}
    new_fields = set(fields)
    return new_fields.issubset(db_fields)


class Null:
    pass


class MetaModel(type):
    """Meta class of Model"""
    inherit_keyword = {'__database__', '__table__', 'Fields'}

    def __new__(mcs, name, bases, attrs):

        for base in bases:
            for _kw in MetaModel.inherit_keyword:
                _v = getattr(base, _kw, None)
                if _kw in attrs or not _v:
                    continue

                if _kw == 'Fields':
                    attrs[_kw].__dict__.update(_v.__dict__)
                else:
                    attrs[_kw] = _v

        _abstract = attrs.get('__abstract__', False)
        _db = attrs.get('__database__')
        if not _db and not _abstract:
            raise ModelAttrNotDefineError('__database__ Not defined')

        _table = attrs.get('__table__')
        if not _table and not _abstract:
            raise ModelAttrNotDefineError('__table__ Not defined')

        _fields = attrs.get('Fields', Null)
        if not _fields and not _abstract:
            raise ModelAttrNotDefineError('Fields Not defined')

        _primary_key = getattr(_fields, 'primary_key', ['id'])

        _fields_data = {}
        for _field_name, _field_default in _fields.__dict__.items() or []:
            if _field_name.startswith("__"):
                continue
            _fields_data[_field_name] = _field_default

        if not _abstract and not validate_model_and_fields(_db, _table, _fields_data.keys()):
            raise ModelAttrNotDefineError

        new_cls = type.__new__(mcs, name, bases, attrs)
        new_cls.__table__ = _table
        new_cls.__database__ = _db
        new_cls._fields_data = copy.deepcopy(_fields_data)
        new_cls._primary_key = _primary_key

        return new_cls


class Model(metaclass=MetaModel):
    __abstract__ = True

    def __init__(self, **kw):
        self._changed_fields = list()
        self._new_record = kw.get('new_record', True)
        self._fields_data = self._fields_data.copy()

        _fields_data_keys = self._fields_data.keys()
        for key, value in kw.items():
            if key in _fields_data_keys:
                if self._new_record:
                    self._changed_fields.append(key)
                elif isinstance(value, bytes):
                    value = value.decode('utf-8')
                self._fields_data[key] = value
                self.__dict__[key] = value

        self.condition_str_tuple = kw.get('condition_str_tuple', tuple())
        self.condition_kw = kw.get('condition_kw', {})

    def __setattr__(self, name, value):
        if name in self._fields_data:
            self._changed_fields.append(name)
            self._fields_data[name] = value

        super().__setattr__(name, value)

    @classmethod
    def get(cls, *condition_str, **condition_kw):
        return Query(cls, db=cls.__database__, condition_str_tuple=condition_str, condition_dict=condition_kw).first()

    @classmethod
    def where(cls, *condition_str_tuple, **condition_kw):
        return Query(cls, db=cls.__database__, condition_str_tuple=condition_str_tuple, condition_dict=condition_kw)

    @classmethod
    def raw(cls, sql, *params, model_instance=False):
        query = Query(cls, db=cls.__database__)
        query.raw_sql(sql, params)
        return query.yield_all(return_model_instance=model_instance)

    @property
    def _primary_key_kv(self):
        return {k: getattr(self, k, None) or self._fields_data[k] for k in self._primary_key}

    def _after_update(self, updated_kv=None):
        self._new_record = False
        if not updated_kv:
            for k, v in updated_kv.items():
                setattr(self, str(k), v)
        self._set_default_value()
        self._changed_fields = []

    def _set_default_value(self):
        for key, value in self._fields_data.items():
            if hasattr(self, key):
                continue
            if callable(value):
                value = value()
            setattr(self, key, value)

    def update(self, **kw):
        query = Query(self, db=self.__database__, condition_dict=self._primary_key_kv)
        self._sync_callable_value(kw)
        ret = query.update(**kw)
        if ret <= 0:
            return ret

        self._set_default_value()
        for k, v in kw.items():
            if k not in self._fields_data:
                continue
            setattr(self, str(k), v)
        self._after_update(kw)
        return ret

    def save(self):
        query = Query(self, db=self.__database__, condition_dict=self._primary_key_kv if not self._new_record else {})
        if not self._new_record:
            _fields_kv = {k: getattr(self, k) for k in self._changed_fields}
        else:
            _fields_kv = {k: self._fields_data[k] for k in self._fields_data}

        self._sync_callable_value(_fields_kv)
        query.update(new_record=self._new_record, **_fields_kv)

        # update primary key value
        for _pk in self._primary_key:
            setattr(self, _pk, query.db.cursor.lastrowid)

        self._after_update(updated_kv=_fields_kv)
        return self

    def _sync_callable_value(self, kw=None):
        if not kw:
            return
        kw_copy = kw.copy()
        for k, v in kw_copy.items():
            if callable(v):
                kw[k] = v()
        if 'update_time' in self._fields_data:
            kw['update_time'] = datetime.datetime.now()
        if 'create_time' in kw and callable(kw['create_time']):
            kw['create_time'] = kw['create_time']()

    def delete(self):
        query = Query(self, db=self.__database__, condition_dict=self._primary_key_kv)
        return query.delete()

    def __str__(self):
        return '{}({})'.format(self.__class__.__name__, ', '.join(['{}={}'.format(*item) for item in self.__dict__.items() if item[0] in self._fields_data]))

    __repr__ = __str__
